查看原文
其他

深入剖析Classloader(二)-根类加载器,扩展类加载器与系统类加载器

2017-03-21 IT哈哈

类的加载的最终产品是位于堆(heap)中的class对象

Class对象封装了类在方法区内的数据结构并向Java程序员提供访问方法区内的数据结构的接口。而反射就像一面镜子一样可这个接口是反射的接口,所以我们可以通过反射获得这个类的方法和属性,包括私有的方法和属性!

下面我们来详细的介绍一下类加载器

类的加载器有两种

    1、Java虚拟机自带的加载器

    2、 用户自定义类加载器

而Java虚拟机自带的加载器又包括3种类加载器

   根类加载器(Bootstrap)

   扩展类加载器(Extension)

   系统类加载器(Ststem)

系统类加载器又称为应用类加载器

其中扩展类加载器和系统类加载器是使用Java实现的。而根加载器是使用C++实现的,JVM的API也没有暴露根类加载器,程序员无法在Java代码中获取根加载器。

用户自定义类加载器是用户自己写的类加载器,但是必须继承java.lang.ClassLoader这个类,用户可以自定义类的加载方式!

我们先来看下类加载器的相关API


 

 每个Class对象都包含了一个对定义的Classloader的定义,也就是说通过Class我们可以拿到对应的Classloader,那我们再来看一下Class这个对象如何拿到Classloader。 

 Class对象有一个getClassLoader的方法用于返回该类的类加载器,但有些实现可能使用null来标识引导类加载器(根类加载器)。也就是说当我们使用根加载器加载的对象使用此方法获取到的ClassLoader是null,为什么是这样呢?前面我们也已经说了,根类加载器是使用C++编写的,JVM不能够也不允许程序员获取该类,所以返回的是null,下面还有一句,如果此对象表示的是一个基本类型或void,则返回null,其实进一步的含义就是:Java中所有的基本数据类型都是由根加载器加载的!(JDK1.5以后将void纳入为基本数据类型)!  

下面我们通过代码来具体来看一下类的先关加载器!(看下面一段代码)

package com.yhj.jvm.classloader.test;

/**

 * @Description:ClassLoader测试与探究

 * @Author YHJ  create at 2011-6-25 下午04:08:32

 * @FileName com.yhj.jvm.classloader.test.ClassLoaderTest.java

 */

publicclass ClassLoaderTest {

    /**

     * @Description:main函数,启动入口

     * @param args

     * @author YHJ create at 2011-6-25 下午04:08:57

     * @throws ClassNotFoundException

     */

    publicstaticvoid main(String[] args) throws ClassNotFoundException {

       System.out.println(Class.forName("java.lang.String").getClassLoader());

    }

}


刚才我们已经说了,基本数据类型使用根类加载器加载的,因此本类中java.lang.String获取类加载器返回的结果应该是null。


 我们修改一下代码看我们自己写的类是哪个类加载器加载的?


package com.yhj.jvm.classloader.test;

/**

 * @Description:ClassLoader测试与探究

 * @Author YHJ  create at 2011-6-25 下午04:08:32

 * @FileName com.yhj.jvm.classloader.test.ClassLoaderTest.java

 */

publicclass ClassLoaderTest {

    /**

     * @Description:main函数,启动入口

     * @param args

     * @author YHJ create at 2011-6-25 下午04:08:57

     * @throws ClassNotFoundException

     */

    publicstaticvoid main(String[] args) throws ClassNotFoundException {

    System.out.println(Class.forName("com.yhj.jvm.classloader.test.ClassLoaderTest").getClassLoader());

    }

}


运行结果是什么呢?


 我们可以看到运行结果是

AppClassLoader执行的,即应用类加载器(系统类加载器)!  

好,具体的细节我们接下来再说!我们先来看一下相关的一些基础内容!

类的加载时机:我们前面提到,所有的类或接口都是在Java虚拟机首次“主动使用”的时候才会初始化,类从class文件到内存经过了“加载”、“连接”、“初始化”,类是在首次主动使用的时候进行初始化,那他是什么时候加载的呢?

类的加载并不需要到该类或接口被首次主动使用的时候才加载,JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载工程中遇到了.class文件缺失或存在错误,则类加载器必须在程序主动使用该类的时候报告LinkageError错误,如果这个类一直没有被程序主动使用,则该类加载器不会报告错误!即使不存在这个类也不会报告错误!

这时候不是异常,是错误哦,看清楚了哦!

一般我们都是和Exception打交道,和Error打交道不太多!打不是Error是由JVM;来处理的!我们来看一下LinkageError这个类的API。


 我们看到他是继承自Error的,LinkageError 的子类指示一个类在一定程度上依赖于另一个类;但是,在编译前一个类之后,后一个类发生了不相容的改变。什么意思呢?举个例子:我们有两个类A和B,B继承自A,我们就说B依赖于A,假设我们都通过JDK1.6编译完成了,但是A又通过JDK1.5重新编译了一次,理论上讲程序是可以正常运行的,但是这时候就会出现新编译的A不兼容B中指定的A,就会抱着个一个错误!  

我们在做web开发的过程中,假设我们的程序是在JDK1.6上编译完成,拿到服务器JDK1.5的环境下运行就可能报出这么一个错误!但是反过来,我们拿1.5编译通过的程序放在1.6上运行,是可以的。因为程序都是向前兼容的,但是从1.6拿到1.5,有可能是好的,也有可能就报出这么一个错误!

类加载完毕之后进入到连接阶段,连接就是将已读入到内存的类的二进制数据合并到虚拟机的运行时环境中去!在连接之前,所有的class文件都是单个的文件,之间没有任何联系,只有JVM把他们连接起来,才能将他们之间的关系有机的结合起来!

类的验证包括一下四个方面

类文件的结构检查

语义检查

字节码验证

二进制兼容性的验证

JDK为了保证class文件的安全性,在加载完成之后又进行了一系列的验证,这些个验证很多在编译的时候已经做过了,但是我们前面已经提到了,很多class字节码文件不是通过javac实现的,例如eclipse就是通过JTA实现的,而我们也可以自己随便写一个.class的文件让他加载,因此为了安全期间,JVM又进行了一次校验!

具体参加《深入Java虚拟机第二版》,如下

 类的准备阶段

 类的解析阶段

 

 这里我们解释一下符号引用和直接引用,在Java语言里面我们说是没有指针的,但是我们看到上图中,car调用了car类定义的run方法,而worker类中是没有car的run的,因此在运行的时候JVM把这个有一个符号和银行替换为一个指针(这里指替换为真正的由C++底层实现的指针),而我们java里面看到的就是符号引用,实际C++执行的才是指针,我们称之为直接引用!

类的初始化

这个时候我们再来看上次那段比较诡异的代码

package com.yhj.jvm.classloader;

/**

 * @Description:单例初始化探究

 * @Author YHJ  create at 2011-6-4 下午08:31:19

 * @FileName com.yhj.jvm.classloader.ClassLoaderTest.java

 */

class Singleton{

    privatestatic Singleton singleton=new Singleton();

    privatestaticintcounter1;

    privatestaticintcounter2 = 0;

    public Singleton() {

       counter1++;

       counter2++;

    }

    publicstaticint getCounter1() {

       returncounter1;

    }

    publicstaticint getCounter2() {

       returncounter2;

    }

    /**

     * @Description:实例化

     * @return

     * @author YHJ create at 2011-6-4 下午08:34:43

     */

    publicstatic Singleton getInstance(){

       returnsingleton;

    } 

}

/**

 * @Description: 测试启动类

 * @Author YHJ  create at 2011-6-4 下午08:35:13

 * @FileName com.yhj.jvm.classloader.ClassLoaderTest.java

 */

publicclass ClassLoaderTest {

    /**

     * @Description:启动类

     * @param args

     * @author YHJ create at 2011-6-4 下午08:30:12

     */

    @SuppressWarnings("static-access")

    publicstaticvoid main(String[] args) {

       Singleton singleton=Singleton.getInstance();

       System.out.println("counter1:"+singleton.getCounter1());

       System.out.println("counter2:"+singleton.getCounter2());

       System.out.println(singleton.getClass().getClassLoader());

    } 

}


我们调用Singleton singleton=Singleton.getInstance();调用Singleton的静态方法,相当于主动使用了类Singleton,因此Singleton被初始化!

当我们看到显示的是

privatestatic Singleton singleton=new Singleton();

    privatestaticintcounter1;

    privatestaticintcounter2 = 0;


这样的时候,顺序执行,先赋予初始值,singleton为null,counter1为0,counter2为0,然后顺序对singleton赋予正确的值new Singleton(),执行构造函数,counter1增加变为1,然后counter2变为1,然后继续执行初始化,将counter2赋值为正确的值,将counter2修改为0,因此运行结果是1、0

反过来

    privatestaticintcounter1;

    privatestaticintcounter2 = 0;

    privatestatic Singleton singleton=new Singleton();

先赋予初始值counter1为0,counter为0,singleton为null,然后对counter2赋值为正确的值,counter2为0,然后对singleton执行初始化赋予正确的值new Singleton(),执行构造函数,counter1为1,counter2为1,因此执行结果是1、1

到此,是不是感觉前面的问题豁然开朗了呢?


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存